/**
 * DPIS server implementation class. It extends StationInterfaceIDLPOA.
 */
package com.comp6231.replica2.server;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;

import com.comp6231.replica2.server.helper.CountObject;
import com.comp6231.replica2.server.helper.IndividualsStatus;
import com.comp6231.replica2.server.helper.Log;
import com.comp6231.replica2.server.helper.StationID;
import com.comp6231.replica2.server.helper.StationInterfaceIDLPOA;
import com.comp6231.replica2.server.helper.Utility;

/**
 * @author Farzana Alam
 * 
 */
public class StationImpl extends StationInterfaceIDLPOA {

	// CR id number
	private static volatile int lastCRecordNumber = 0;
	// MR id number
	private static volatile int lastMRecordNumber = 0;

	// number of records
	private volatile int numberOfRecords = 0;
	// this is my station id (i.e SPVM, SPL, SPB)
	private StationID myStationID;

	/**
	 * Returns this station id.
	 * 
	 * @return StationID
	 */
	public StationID getMyStationID() {
		return myStationID;
	}

	// UDP Server running on this port;
	private int myPort;

	// this data structure used to store all records
	private Hashtable<String, ArrayList<Record>> recordsDB;
	// this data structure used to store record id and last name mapping
	private Hashtable<String, String> recordIDLastName;

	/**
	 * This constructor initializes recordsDB and recordIDLastName
	 * 
	 * @param server1StationID
	 * @param serverPort
	 */

	public StationImpl(StationID server1StationID, int serverPort) {

		this.myStationID = server1StationID;
		this.myPort = serverPort;

		recordsDB = new Hashtable<String, ArrayList<Record>>();
		recordIDLastName = new Hashtable<String, String>();
		fillUpTheTable();
		startCountServer();
	}

	/**
	 * @return {@link Log}
	 */
	@Override
	public Log createCRecord(String badgeID, String firstName, String lastName,
			String description, IndividualsStatus status) {
		Log log;
		// Get the operation time
		String timeStamp = getActionDate();
		// Get the formatted Criminal Record ID
		String recordID = recordIDMaker(RecordType.CR, lastCRecordNumber++);

		if (recordID.length() == 0) {
			log = new Log(badgeID, myStationID.toString(),
					"Record not created, database full ", timeStamp, "Fail",
					"N/A", "N/A");
			Utility.writeLog(log, myStationID + ".log");
			return log;
		}
		// Create a criminal record object
		CriminalRecord cRecord = new CriminalRecord(badgeID, recordID,
				lastName, firstName, description, status);
		// Store the record into the list
		insertToDB(cRecord);

		log = new Log(badgeID, myStationID.toString(),
				"Criminal record created for " + lastName, timeStamp,
				"Successful", recordID, IndividualsStatus.from_int(
						status.value()).toString());
		Utility.writeLog(log, myStationID + ".log");

		return log;
	}

	/**
	 * 
	 * @see com.farzana.dpis.server.StationInterfaceRMI#createMRecord(java.lang.String,
	 *      java.lang.String, java.lang.String, java.util.Date,
	 *      java.lang.String, java.lang.String)
	 */
	@Override
	public Log createMRecord(String badgeID, String firstName, String lastName,
			String address, String lastDate, String lastLocation,
			IndividualsStatus status) {
		Log log;

		// Get the operation time
		String timeStamp = getActionDate();
		// Get the formatted missing Record ID
		String recordID = recordIDMaker(RecordType.MR, lastMRecordNumber++);

		if (recordID.length() == 0) {
			log = new Log(badgeID, myStationID.toString(),
					"Record not created, database full ", timeStamp, "Fail",
					"N/A", "N/A");
			Utility.writeLog(log, myStationID + ".log");
			return log;
		}
		// Create a missing record object
		MissingRecord mRecord = new MissingRecord(badgeID, recordID, lastName,
				firstName, address, lastDate, lastLocation, status);

		// Store the record into the list
		insertToDB(mRecord);
		log = new Log(badgeID, myStationID.toString(),
				"Missing record created for " + lastName, timeStamp,
				"Successful", recordID, IndividualsStatus.from_int(
						status.value()).toString());
		Utility.writeLog(log, myStationID + ".log");

		return log;
	}

	/**
	 * 
	 * @see com.farzana.dpis.server.StationInterfaceRMI#getRecordCounts()
	 */
	@Override
	public CountObject getRecordCounts(String badgeID) {
		/*
		 * This method is used to collect number of records from other servers
		 */
		String count1 = "";
		String count2 = "";

		// Get the operation time
		String timeStamp = getActionDate();
		// Montreal(SPVM): 4567, Longuil(SPL): 5678, Brossard(SPB): 6789
		switch (myStationID) {
		case SPVM:
			// Get the record count from Longuil Server
			count1 = OperationClient.getOthersOperation("localhost", 5678,
					"COUNT");
			// Get the record count from Brossard Server
			count2 = OperationClient.getOthersOperation("localhost", 6789,
					"COUNT");
			break;
		case SPL:
			// Get the record count from Montreal Server
			count1 = OperationClient.getOthersOperation("localhost", 4567,
					"COUNT");
			// Get the record count from Brossard Server
			count2 = OperationClient.getOthersOperation("localhost", 6789,
					"COUNT");
			break;
		case SPB:
			// Get the record count from Longuil Server
			count1 = OperationClient.getOthersOperation("localhost", 5678,
					"COUNT");
			// Get the record count from Montreal Server
			count2 = OperationClient.getOthersOperation("localhost", 4567,
					"COUNT");
			break;
		default:
			count1 = "";
			count2 = "";
		}

		// Get the local count and add them with others count
		String counts = getLocalCount() + ", " + count1 + ", " + count2;

		Log log = new Log(badgeID, myStationID.toString(), "Count operation",
				"\t\t" + timeStamp, "Successful", "N/A", "N/A");
		Utility.writeLog(log, myStationID + ".log");

		// Create the count object with count info and Log
		CountObject countObj = new CountObject(counts, log);

		return countObj;

	}

	/**
	 * @see com.farzana.dpis.server.StationInterfaceRMI#editCRecord(java.lang.String,
	 *      java.lang.String, java.lang.String)
	 */
	@Override
	public Log editRecord(String badgeID, String lastName, String recordID,
			IndividualsStatus newStatus) {
		Log log;

		// Get the operation time
		String timeStamp = getActionDate();

		// Get the record prefix (i.e CR or MR)
		String prefix = recordID.substring(0, 2);
		prefix = prefix.toUpperCase();

		// If MR record, no edit allowed
		if (prefix.equalsIgnoreCase("MR")) {
			log = new Log(badgeID, myStationID.toString(),
					"Record not found for " + lastName, timeStamp, "Fail",
					recordID, "N/A");
			Utility.writeLog(log, myStationID + ".log");
			return log;
		}

		// Get the first letter of the last name
		String key = lastName.substring(0, 1).toUpperCase();

		// Retrieve the list for that key
		ArrayList<Record> existedList = recordsDB.get(key);

		// Lock only that list so that other thread cannot modify this list
		synchronized (existedList) {
			Iterator<Record> it = existedList.iterator();
			while (it.hasNext()) {
				Record r1 = it.next();
				if (r1.getRecordID().equalsIgnoreCase(recordID)
						&& r1.getLastName().equalsIgnoreCase(lastName)) {
					r1.setStatus(newStatus);
					log = new Log(badgeID, myStationID.toString(),
							"Record modified for " + lastName, timeStamp,
							"Successful", recordID, IndividualsStatus.from_int(
									r1.getStatus().value()).toString());
					Utility.writeLog(log, myStationID + ".log");
					return log;
				}
			}

			log = new Log(badgeID, myStationID.toString(),
					"Record not found for " + lastName, "\t" + timeStamp,
					"Fail", recordID, "N/A");
			Utility.writeLog(log, myStationID + ".log");
		}
		return log;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.farzana.dpis.common.StationInterfaceIDLOperations#transferReecord
	 * (java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public Log transferReecord(String badgeID, String recordID,
			String remoteStationServerName) {
		/*
		 * Transfer a record from this server to another server
		 */
		Log log;

		// If badgeID is not in this server then no transfer allowed
		if (!badgeID.toUpperCase().startsWith(
				myStationID.toString().toUpperCase())) {

			log = new Log(badgeID, myStationID.toString(),
					"It is not your server", getActionDate(), "Fail", recordID,
					"N/A");
			Utility.writeLog(log, myStationID.toString() + ".log");
			System.out.println("Cannot transfer! Officer " + badgeID
					+ " - is in the wrong server: " + myStationID.toString());
			return log;
		}

		// If remote server is this server then no transfer needed
		if (remoteStationServerName.equalsIgnoreCase(myStationID.toString())) {
			log = new Log(badgeID, myStationID.toString(),
					"Same server transfer not possible", getActionDate(),
					"Fail", recordID, "N/A");
			Utility.writeLog(log, myStationID.toString() + ".log");
			System.out.println("Officer " + badgeID
					+ " - same server transfer not possible - Server: "
					+ myStationID.toString());
			return log;
		}

		// Get the remote station server reference
		// StationInterfaceIDL stationServer =
		// getCORBAObj(remoteStationServerName);

		// Get the whole record for that particular record ID
		Record record = getTheRecord(recordID);

		// System.out.println(myRecord + " in tr");

		if (record == null) {
			log = new Log(badgeID, myStationID.toString(),
					"Record not found to transfer", getActionDate(), "Fail",
					recordID, "N/A");
			Utility.writeLog(log, myStationID.toString() + ".log");
			return log;

		}
		// transfer the record to the remote server
		// log = stationServer.transfer(record.toString());
		String transferResult = transferToRemote(remoteStationServerName,
				record.toString());

		log = new Log(badgeID, myStationID.toString(),
				"Record not found to transfer", getActionDate(),
				transferResult.trim(), recordID, "N/A");

		// In case of failure to transfer does not remove the record from my
		// list
		if (log.operationStatus.equalsIgnoreCase("Fail")) {
			// revert the record from my list
			revertTheRecord(record);
			log.operationDescription = "Failed to transfer to"
					+ remoteStationServerName + " server";
			Utility.writeLog(log, myStationID.toString() + ".log");
			return log;
		}

		log.operationDescription = "Transfered to " + remoteStationServerName
				+ " server";
		Utility.writeLog(log, myStationID.toString() + ".log");
		return log;
	}

	// Method is used to transfer to other server
	private String transferToRemote(String remoteServer, String record) {
		String result = "";
		switch (remoteServer) {
		case "SPVM":
			// transfer to Montreal Server
			result = OperationClient.getOthersOperation("localhost", 4567,
					"TRANSFER" + record);
			break;
		case "SPL":
			// transfer to Longuiel Server
			result = OperationClient.getOthersOperation("localhost", 5678,
					"TRANSFER" + record);
			break;
		case "SPB":
			// transfer to Brossard Server
			result = OperationClient.getOthersOperation("localhost", 6789,
					"TRANSFER" + record);
			break;
		default:
			result = "";

		}
		return result;
	}

	private void revertTheRecord(Record record) {
		/*
		 * Remove that particular record from the list
		 */
		String key = recordIDLastName.get(record.getRecordID());
		ArrayList<Record> recordList = recordsDB.get(key);
		synchronized (recordList) {
			recordList.add(record);
			numberOfRecords++;

		}

	}

	// private StationInterfaceIDL getCORBAObj(String remoteStationServerName) {
	// /*
	// * Get the reference of the remote object
	// */
	// remoteStationServerName = remoteStationServerName.toUpperCase();
	// StationID stationID;
	// switch (remoteStationServerName) {
	// case "SPVM":
	// stationID = StationID.SPVM;
	// break;
	// case "SPL":
	// stationID = StationID.SPL;
	// break;
	// case "SPB":
	// stationID = StationID.SPB;
	// break;
	// default:
	// stationID = this.myStationID;
	// break;
	// }
	// return ClientCORBAObject.getCORBAObj(stationID);
	// }

	private Record getTheRecord(String recordID) {
		/*
		 * Get the record of that record ID
		 */
		String key = recordIDLastName.get(recordID);
		if (key == null)
			return null;

		// System.out.println("recordDB Key - " + key + " record id: " +
		// recordID);

		ArrayList<Record> recordList = recordsDB.get(key);
		Record record = null;
		if (recordList == null)
			return null;

		synchronized (recordList) {
			for (int i = 0; i < recordList.size(); i++) {
				if (recordList.get(i).getRecordID().equalsIgnoreCase(recordID)) {
					record = recordList.get(i);
					recordList.remove(i);
					numberOfRecords--;
					System.out.println("Record id:  " + recordID
							+ " removed from " + myStationID);
					break;
				}
			}
		}

		return record;
	}

	/**
	 * This method is used to receive record from other server
	 * 
	 * @param myRecord
	 * @return
	 */
	public String recieveFromRemote(String myRecord) {
		String status = "";
		Log log = null;
		/*
		 * Insert myRecord into this server
		 */
		if (myRecord.startsWith("MR"))
			log = receivedMR(myRecord);
		if (myRecord.startsWith("CR"))
			log = receivedCR(myRecord);

		log.operationDescription = "Received From Other Server";
		Utility.writeLog(log, myStationID.toString().trim() + ".log");

		if (log.operationStatus.equalsIgnoreCase("Successful"))
			status = "Successful";
		else
			status = "Fail";

		return status;
	}

	private Log receivedCR(String myRecord) {
		// the following string format is expected as myRecord
		// recoredID:lastName:firstName:description:status:badgeID
		// temp[0]: temp[1]: temp[2]: temp[3] :temp[4]:temp[5]

		String[] temp = myRecord.split("##");
		IndividualsStatus status = convertToStatus(temp[4]);

		System.out.println("Received ID " + temp[0] + " from Server: "
				+ getServerNameFromBadge(temp[5].trim()) + " into server: "
				+ myStationID);

		Log log;
		// Get the operation time
		String timeStamp = getActionDate();
		// Get the formatted Criminal Record ID
		String recordID = temp[0].trim();//recordIDMaker(RecordType.CR, ++lastCRecordNumber);

		if (recordID.length() == 0) {
			log = new Log(temp[5].trim(), myStationID.toString(),
					"Record not created, database full ", timeStamp, "Fail",
					"N/A", "N/A");
			Utility.writeLog(log, myStationID + ".log");
			return log;
		}
		// Create a criminal record object
		CriminalRecord cRecord = new CriminalRecord(temp[5].trim(), recordID,
				temp[1], temp[2], temp[3], status);
		// Store the record into the list
		insertToDB(cRecord);

		log = new Log(temp[5].trim(), myStationID.toString(), "Received from: "
				+ getServerNameFromBadge(temp[5]) + ", Received ID: " + temp[0]
				+ " for " + temp[1], timeStamp, "Successful", recordID,
				IndividualsStatus.from_int(status.value()).toString());
		Utility.writeLog(log, myStationID + ".log");

		return log;

	}

	private Log receivedMR(String myRecord) {
		// the following string format is expected as myRecord
		// recoredID:lastName:firstName:address:lastDate:lastLocation:status:badgeID
		// temp[0]: temp[1]: temp[2]: temp[3]: temp[4]: temp[5]: temp[6]:
		// temp[7]
		String[] temp = myRecord.split("##");
		Log log;
		IndividualsStatus status = convertToStatus(temp[6]);

		System.out.println("Received ID " + temp[0] + " from Server: "
				+ getServerNameFromBadge(temp[7]) + " into server: "
				+ myStationID);

		// Get the operation time
		String timeStamp = getActionDate();
		// Get the formatted missing Record ID
		String recordID = temp[0].trim();//recordIDMaker(RecordType.MR, ++lastMRecordNumber);

		if (recordID.length() == 0) {
			log = new Log(temp[7].trim(), myStationID.toString(),
					"Record not created, database full ", timeStamp, "Fail",
					"N/A", "N/A");
			Utility.writeLog(log, myStationID + ".log");
			return log;
		}
		// Create a missing record object
		MissingRecord mRecord = new MissingRecord(temp[7].trim(), recordID,
				temp[1], temp[2], temp[3], temp[4], temp[5], status);

		// Store the record into the list
		insertToDB(mRecord);
		log = new Log(temp[7].trim(), myStationID.toString(), "Received from: "
				+ getServerNameFromBadge(temp[7]) + ", Received ID: " + temp[0]
				+ " for " + temp[1], timeStamp, "Successful", recordID,
				IndividualsStatus.from_int(status.value()).toString());
		Utility.writeLog(log, myStationID + ".log");

		return log;

	}

	private String getServerNameFromBadge(String badgeID) {
		/*
		 * retrieve the server name form badge id
		 */
		if (badgeID.startsWith("SPVM"))
			return "SPVM";
		else if (badgeID.startsWith("SPL"))
			return "SPL";
		else
			return "SPB";
	}

	private IndividualsStatus convertToStatus(String statusString) {
		/*
		 * convert the status string into IndividualStatus type
		 */
		IndividualsStatus status = null;

		switch (statusString.toUpperCase()) {
		case "MISSING":
			status = IndividualsStatus.from_int(0);
			break;
		case "FOUND":
			status = IndividualsStatus.from_int(1);
			break;
		case "ON_THE_RUN":
			status = IndividualsStatus.from_int(2);
			break;
		case "CAPTURED":
			status = IndividualsStatus.from_int(3);
			break;

		}
		return status;
	}

	private String recordIDMaker(RecordType recordType, int id) {
		// Make a record id by mixing string and digits with predefined format
		if (id > 9999)
			return "";

		String resultID = recordType.name() + String.format("1%04d", id);

		return resultID;
	}

	private void fillUpTheTable() {
		// Fill up the hashtable with all the keys and empty list
		for (int i = 65; i <= 90; i++) {
			recordsDB
					.put(Character.toString((char) i), new ArrayList<Record>());

		}

	}

	private void insertToDB(Record record) {
		// This method is used to store the record into hashtable
		String key = record.getLastName().substring(0, 1).toUpperCase();

		ArrayList<Record> existedList = recordsDB.get(key);

		synchronized (existedList) {
			existedList.add(record);
			numberOfRecords++;
		}

		recordIDLastName.put(record.getRecordID(), key);

	}

	protected String getLocalCount() {
		// returns the local server record count
		return this.myStationID + ": " + numberOfRecords;

	}

	private void startCountServer() {
		// Start a new UDP server as a thread to serve count for other server.
		OperationServer countServer = new OperationServer(this, myPort);
		new Thread(countServer).start();
	}

	private String getActionDate() {
		// taking the system date in order to use as an operation date.
		DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		Date date = new Date();
		String timeStamp = dateFormat.format(date);
		return timeStamp;
	}

}
